sartUP — Setup Iniziale (Blade + Auth Nativa + Spatie Ruoli “minimal”)
sartUP — Setup Iniziale (Blade + Auth Nativa + Spatie Ruoli “minimal”)
Obiettivo: partire con il piede giusto: Laravel 11 + Blade, autenticazione nativa, Spatie/laravel-permission per soli ruoli (permessi granulari attivabili in futuro), menu dinamico amministrabile e placeholder Industria 4.0 → Report → Macchine → Elenco macchine collegate.> Questo documento è pensato per Cursor come contesto operativo: include environment, pacchetti, tasks, snippet e criteri di accettazione.
---
0) Architettura di riferimento (VPS cPanel)
- Admin Container: topbar orizzontale (L1) + sidebar sinistra (L2/3/4), Blade.
- Auth nativa: login, logout, forgot/reset password (+ opzionale email verification).
- Ruoli con Spatie:
super-admin,admin,operator,maintenance,viewer; ruolo attivo selezionabile se l'utente ne ha >1. - Menu dinamico su DB:
menus,menu_items, filtrati per ruolo/permessi (inizialmente per ruoli). - Placeholder moduli: Dashboard, Industria 4.0 → Report → Macchine → Elenco macchine collegate.
- Servizi VPS: MySQL, Redis (opzionale), file storage locale, cron jobs, SSL automatico.
- VPS con cPanel (AlmaLinux)
- PHP 8.2+, Composer 2.7+
- Node 20 + npm/Vite
- MySQL 8.0+
- SSL/TLS automatico (AutoSSL cPanel)
- Redis (opzionale, per cache/queue)
- PHP-FPM (gestito da cPanel)
- MySQL (database cPanel)
- Redis (se disponibile, altrimenti file/database cache)
- Cron jobs (Laravel Scheduler)
- Storage locale o S3-compatible (opzionale)
- Login/logout/reset funzionanti (Blade).
- Seeder: creati ruoli + utente super-admin.
- Role selector post-login per utenti con >1 ruolo; switch ruolo attivo in sessione.
- Menu dinamico in DB; render topbar + sidebar; voce "Industria 4.0 → Report → Macchine → Elenco macchine collegate" visibile al ruolo indicato.
- Cron job configurato per Laravel Scheduler.
- Queue worker attivo (se necessario).
- [ ] Setup database MySQL in cPanel
- [ ] Configurazione
- [ ] Migrazioni
- [ ] Install Spatie + publish + migrazioni
- [ ] Seeder:
- [ ] Middleware
- [ ] Controller & Blade auth nativa (login, forgot/reset) + layout admin
- [ ] Service
- [ ] Rotte/controller/view i40.machines.connected (placeholder)
- [ ] Cron job per
- [ ] Queue worker via cron o supervisord (opzionale)
---
1) Ambiente & servizi VPS
1.1 Requisiti
1.2 Configurazione ambiente VPS
> L'applicazione gira direttamente sul VPS senza containerizzazione.Servizi disponibili:
.env chiavi principali:
``dotenv
APP_ENV=production
APP_URL=https://sartup.it
APP_KEY=
DB_HOST=localhost DB_DATABASE=cpanel_user_sartup DB_USERNAME=cpanel_user_xxxxx DB_PASSWORD=secure_password
CACHE_DRIVER=file QUEUE_CONNECTION=database
Se Redis disponibile:
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=localhost
FILESYSTEM_DISK=local
Se usi S3:
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=sartup
`---
2) Pacchetti & configurazione
2.1 Installazione Spatie (ruoli solo)
`bash
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate
`Config:
config/permission.php (default va bene).
Trait nel Model User: use Spatie\Permission\Traits\HasRoles;2.2 Auth nativa (senza Breeze)
Rotte, controller e Blade scritti ad hoc (vedi §4). Opzionale: email verification.---
3) Database: menu dinamico + ruoli
3.1 Migrazioni Menu (DB-first)
> Task per Cursor: creare migrazioni Eloquent per gli schemi sotto.`sql
CREATE TABLE menus (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(64) UNIQUE NOT NULL, -- 'admin_main'
description VARCHAR(255) NULL,
is_active TINYINT(1) DEFAULT 1,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL
);CREATE TABLE menu_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
menu_id BIGINT NOT NULL,
parent_id BIGINT NULL,
label VARCHAR(100) NOT NULL,
route_name VARCHAR(120) NULL, -- priorità a route_name, fallback url
url VARCHAR(255) NULL,
icon VARCHAR(60) NULL,
description VARCHAR(255) NULL,
order_index INT DEFAULT 0,
is_visible TINYINT(1) DEFAULT 1,
required_roles JSON NULL, -- es. ["admin","maintenance"]
required_permissions JSON NULL, -- es. ["machines.view"]
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
CONSTRAINT fk_menu FOREIGN KEY (menu_id) REFERENCES menus(id),
CONSTRAINT fk_parent FOREIGN KEY (parent_id) REFERENCES menu_items(id)
);
`3.2 Seeder Ruoli & Super Admin
`php
// database/seeders/RolesSeeder.php
use Spatie\Permission\Models\Role;
class RolesSeeder extends Seeder {
public function run(): void {
foreach (['super-admin','admin','operator','maintenance','viewer'] as $r) {
Role::firstOrCreate(['name' => $r]);
}
}
}// database/seeders/SuperAdminSeeder.php
use App\Models\User;
use Spatie\Permission\Models\Role;
class SuperAdminSeeder extends Seeder {
public function run(): void {
$u = User::firstOrCreate(
['email' => 'root@sartup.local'],
['name' => 'Root', 'password' => bcrypt('ChangeMe!')]
);
$u->assignRole(Role::where('name','super-admin')->first());
}
}
`3.3 Seeder Menu (placeholder + Industria 4.0)
`php
// database/seeders/MenuSeeder.php
use App\Models\Menu;
use App\Models\MenuItem;class MenuSeeder extends Seeder {
public function run(): void {
$admin = Menu::firstOrCreate(['name'=>'admin_main'], ['description'=>'Menu principale admin']);
$dashboard = MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Dashboard'
],['route_name'=>'admin.dashboard','icon'=>'lucide-home','order_index'=>1]);
$ind40 = MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Industria 4.0'
],['icon'=>'lucide-cpu','order_index'=>2]);
$report = MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>$ind40->id,'label'=>'Report'
],['order_index'=>1]);
$macchine = MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>$report->id,'label'=>'Macchine'
],['order_index'=>1]);
MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>$macchine->id,'label'=>'Elenco macchine collegate'
],[
'route_name'=>'i40.machines.connected',
'order_index'=>1,
'required_roles'=>json_encode(['admin','maintenance','super-admin'])
]);
MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Impostazioni'
],['icon'=>'lucide-settings','order_index'=>99,'required_roles'=>json_encode(['super-admin'])]);
}
}
`---
4) Auth nativa + Ruolo attivo
4.1 Rotte base
`php
// routes/web.php
Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login')->middleware('guest');
Route::post('/login', [LoginController::class, 'login'])->name('login.post')->middleware('guest');
Route::post('/logout', [LoginController::class, 'logout'])->name('logout')->middleware('auth');Route::get('/password/forgot', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request');
Route::post('/password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
Route::get('/password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->name('password.reset');
Route::post('/password/reset', [ResetPasswordController::class, 'reset'])->name('password.update');
// Admin area
Route::middleware(['auth','active.role'])->prefix('admin')->group(function () {
Route::get('/', [DashboardController::class,'index'])->name('admin.dashboard');
Route::prefix('i40')->group(function() {
Route::get('/', [I40\HomeController::class,'index'])->name('i40.home');
Route::get('/machines/connected', [I40\MachinesController::class,'connected'])->name('i40.machines.connected');
});
});
`4.2 Middleware “ActiveRole”
`php
// app/Http/Middleware/EnsureActiveRole.php
class EnsureActiveRole {
public function handle($request, Closure $next) {
$user = $request->user();
if (!$user) return redirect()->route('login');
if ($user->roles()->count() <= 1) {
if (!session()->has('active_role') && $user->roles()->exists()) {
session(['active_role' => $user->roles()->first()->name]);
}
return $next($request);
}
if (!session()->has('active_role')) {
return redirect()->route('auth.role.select');
}
return $next($request);
}
}
`4.3 Selettore ruolo (post-login)
`php
// routes/web.php (aggiunte)
Route::middleware(['auth'])->group(function () {
Route::get('/auth/select-role', [RoleSelectorController::class,'show'])->name('auth.role.select');
Route::post('/auth/set-role', [RoleSelectorController::class,'set'])->name('auth.role.set');
});
``php
// app/Http/Controllers/Auth/RoleSelectorController.php
class RoleSelectorController extends Controller {
public function show(Request $r) {
$roles = $r->user()->roles()->pluck('name');
return view('auth.select-role', compact('roles'));
}
public function set(Request $r) {
$r->validate(['role'=>'required|string']);
abort_unless($r->user()->hasRole($r->role), 403);
session(['active_role' => $r->role]);
return redirect()->intended(route('admin.dashboard'));
}
}
``blade
{{-- resources/views/auth/select-role.blade.php --}}
@extends('layouts.app')
@section('content')
<h1>Seleziona ruolo</h1>
<form method="POST" action="{{ route('auth.role.set') }}">
@csrf
<select name="role" required>
@foreach($roles as $role)
<option value="{{ $role }}">{{ ucfirst($role) }}</option>
@endforeach
</select>
<button type="submit">Continua</button>
</form>
@endsection
`Gate super-admin
`php
// app/Providers/AuthServiceProvider.php
Gate::before(function ($user, $ability) {
return $user->hasRole('super-admin') ? true : null;
});
`---
5) Menu dinamico: Model/Service & Rendering
5.1 Model minimi
`php
// app/Models/Menu.php
class Menu extends Model {
protected $fillable = ['name','description','is_active'];
public function items() { return $this->hasMany(MenuItem::class); }
}// app/Models/MenuItem.php
class MenuItem extends Model {
protected $fillable = [
'menu_id','parent_id','label','route_name','url','icon','description',
'order_index','is_visible','required_roles','required_permissions'
];
protected $casts = ['required_roles'=>'array','required_permissions'=>'array'];
public function parent(){ return $this->belongsTo(MenuItem::class,'parent_id'); }
public function children(){ return $this->hasMany(MenuItem::class,'parent_id'); }
}
`5.2 Service per albero filtrato
`php
// app/Services/MenuService.php
class MenuService {
public function forUserMenu(string $menuName, ?User $user): array {
$menu = Menu::where('name',$menuName)->first();
if (!$menu) return [];
$items = $menu->items()->orderBy('order_index')->get()->groupBy('parent_id');
$activeRole = session('active_role'); $filter = function($item) use ($user,$activeRole) {
if (!$item->is_visible) return false;
if ($item->required_roles && $activeRole && !in_array($activeRole, $item->required_roles)) return false;
if ($item->required_permissions) {
foreach ($item->required_permissions as $perm) {
if (!$user || !$user->can($perm)) return false;
}
}
return true;
};
$build = function($parentId) use (&$build, $items, $filter) {
return ($items[$parentId] ?? collect())->filter($filter)->map(function($i) use (&$build) {
return [
'id'=>$i->id,'label'=>$i->label,'route_name'=>$i->route_name,'url'=>$i->url,'icon'=>$i->icon,
'children'=>$build($i->id)->values()->all()
];
});
};
return $build(null)->values()->all();
}
}
`5.3 Layout Blade (contenitore)
`blade
{{-- resources/views/layouts/admin.blade.php --}}
@php($menu = app(\App\Services\MenuService::class)->forUserMenu('admin_main', auth()->user()))
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title','sartUP Admin')</title>
@vite(['resources/css/app.css','resources/js/app.js'])
</head>
<body class="min-h-screen bg-gray-50">
<header class="h-14 shadow flex items-center px-4 bg-white">
<nav class="flex gap-4">
@foreach($menu as $item)
<a href="{{ $item['route_name'] ? route($item['route_name']) : ($item['url'] ?? '#') }}" class="font-medium">
{{ $item['label'] }}
</a>
@endforeach
</nav>
<div class="ml-auto">
<form method="POST" action="{{ route('logout') }}">@csrf<button>Logout</button></form>
</div>
</header>
<div class="flex">
<aside class="w-64 bg-white border-r min-h-[calc(100vh-3.5rem)] p-3">
{{-- sidebar: figli della voce L1 corrente → per demo, mostra tutti i figli del primo item --}}
@if(count($menu))
@foreach(($menu[1]['children'] ?? $menu[0]['children'] ?? []) as $child)
<div class="mb-3">
<div class="font-semibold">{{ $child['label'] }}</div>
@if(count($child['children']))
<ul class="ml-3 list-disc">
@foreach($child['children'] as $sub)
<li><a href="{{ $sub['route_name'] ? route($sub['route_name']) : ($sub['url'] ?? '#') }}">{{ $sub['label'] }}</a></li>
@endforeach
</ul>
@endif
</div>
@endforeach
@endif
</aside>
<main class="flex-1 p-6">
@yield('content')
</main>
</div>
</body>
</html>
`---
6) Placeholder Industrie 4.0
6.1 Rotte & Controller
`php
// app/Http/Controllers/Admin/I40/MachinesController.php
namespace App\Http\Controllers\Admin\I40;
class MachinesController extends Controller {
public function connected() {
return view('admin.i40.machines.connected');
}
}
``blade
{{-- resources/views/admin/i40/machines/connected.blade.php --}}
@extends('layouts.admin')
@section('title','Elenco macchine collegate')
@section('content')
<h1 class="text-xl font-semibold mb-4">Elenco macchine collegate</h1>
<table class="min-w-full bg-white shadow border">
<thead><tr>
<th class="p-2 text-left">Macchina</th>
<th class="p-2 text-left">Protocollo</th>
<th class="p-2 text-left">Stato</th>
<th class="p-2 text-left">Last seen</th>
</tr></thead>
<tbody>
<tr><td class="p-2">—</td><td class="p-2">—</td><td class="p-2">—</td><td class="p-2">—</td></tr>
</tbody>
</table>
@endsection
`---
7) Criteri di accettazione (Fase VPS)
---
8) TODO per Cursor (operativo)
.env per VPS
menus, menu_items
RolesSeeder, SuperAdminSeeder, MenuSeeder
EnsureActiveRole + rotte auth.select-role/auth.set-role
MenuService + rendering topbar/sidebar
php artisan schedule:run` (ogni minuto)
Analisi Codice
Blocco 1 dotenv
APP_ENV=production
APP_URL=https://sartup.it
APP_KEY=
DB_HOST=localhost
DB_DATABASE=cpanel_user_sartup
DB_USERNAME=cpanel_user_xxxxx
DB_PASSWORD=secure_password
CACHE_DRIVER=file
QUEUE_CONNECTION=database
# Se Redis disponibile:
# CACHE_DRIVER=redis
# QUEUE_CONNECTION=redis
# REDIS_HOST=localhost
FILESYSTEM_DISK=local
# Se usi S3:
# FILESYSTEM_DISK=s3
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# AWS_DEFAULT_REGION=us-east-1
# AWS_BUCKET=sartup
Blocco 2 bash
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate
Blocco 3 sql
CREATE TABLE menus (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(64) UNIQUE NOT NULL, -- 'admin_main'
description VARCHAR(255) NULL,
is_active TINYINT(1) DEFAULT 1,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL
);
CREATE TABLE menu_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
menu_id BIGINT NOT NULL,
parent_id BIGINT NULL,
label VARCHAR(100) NOT NULL,
route_name VARCHAR(120) NULL, -- priorità a route_name, fallback url
url VARCHAR(255) NULL,
icon VARCHAR(60) NULL,
description VARCHAR(255) NULL,
order_index INT DEFAULT 0,
is_visible TINYINT(1) DEFAULT 1,
required_roles JSON NULL, -- es. ["admin","maintenance"]
required_permissions JSON NULL, -- es. ["machines.view"]
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
CONSTRAINT fk_menu FOREIGN KEY (menu_id) REFERENCES menus(id),
CONSTRAINT fk_parent FOREIGN KEY (parent_id) REFERENCES menu_items(id)
);
Blocco 4 php
// database/seeders/RolesSeeder.php
use Spatie\Permission\Models\Role;
class RolesSeeder extends Seeder {
public function run(): void {
foreach (['super-admin','admin','operator','maintenance','viewer'] as $r) {
Role::firstOrCreate(['name' => $r]);
}
}
}
// database/seeders/SuperAdminSeeder.php
use App\Models\User;
use Spatie\Permission\Models\Role;
class SuperAdminSeeder extends Seeder {
public function run(): void {
$u = User::firstOrCreate(
['email' => 'root@sartup.local'],
['name' => 'Root', 'password' => bcrypt('ChangeMe!')]
);
$u->assignRole(Role::where('name','super-admin')->first());
}
}
Blocco 5 php
// database/seeders/MenuSeeder.php
use App\Models\Menu;
use App\Models\MenuItem;
class MenuSeeder extends Seeder {
public function run(): void {
$admin = Menu::firstOrCreate(['name'=>'admin_main'], ['description'=>'Menu principale admin']);
$dashboard = MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Dashboard'
],['route_name'=>'admin.dashboard','icon'=>'lucide-home','order_index'=>1]);
$ind40 = MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Industria 4.0'
],['icon'=>'lucide-cpu','order_index'=>2]);
$report = MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>$ind40->id,'label'=>'Report'
],['order_index'=>1]);
$macchine = MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>$report->id,'label'=>'Macchine'
],['order_index'=>1]);
MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>$macchine->id,'label'=>'Elenco macchine collegate'
],[
'route_name'=>'i40.machines.connected',
'order_index'=>1,
'required_roles'=>json_encode(['admin','maintenance','super-admin'])
]);
MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Impostazioni'
],['icon'=>'lucide-settings','order_index'=>99,'required_roles'=>json_encode(['super-admin'])]);
}
}
Blocco 6 php
// routes/web.php
Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login')->middleware('guest');
Route::post('/login', [LoginController::class, 'login'])->name('login.post')->middleware('guest');
Route::post('/logout', [LoginController::class, 'logout'])->name('logout')->middleware('auth');
Route::get('/password/forgot', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request');
Route::post('/password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
Route::get('/password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->name('password.reset');
Route::post('/password/reset', [ResetPasswordController::class, 'reset'])->name('password.update');
// Admin area
Route::middleware(['auth','active.role'])->prefix('admin')->group(function () {
Route::get('/', [DashboardController::class,'index'])->name('admin.dashboard');
Route::prefix('i40')->group(function() {
Route::get('/', [I40\HomeController::class,'index'])->name('i40.home');
Route::get('/machines/connected', [I40\MachinesController::class,'connected'])->name('i40.machines.connected');
});
});
Blocco 7 php
// app/Http/Middleware/EnsureActiveRole.php
class EnsureActiveRole {
public function handle($request, Closure $next) {
$user = $request->user();
if (!$user) return redirect()->route('login');
if ($user->roles()->count() <= 1) {
if (!session()->has('active_role') && $user->roles()->exists()) {
session(['active_role' => $user->roles()->first()->name]);
}
return $next($request);
}
if (!session()->has('active_role')) {
return redirect()->route('auth.role.select');
}
return $next($request);
}
}
Blocco 8 php
// routes/web.php (aggiunte)
Route::middleware(['auth'])->group(function () {
Route::get('/auth/select-role', [RoleSelectorController::class,'show'])->name('auth.role.select');
Route::post('/auth/set-role', [RoleSelectorController::class,'set'])->name('auth.role.set');
});
Blocco 9 php
// app/Http/Controllers/Auth/RoleSelectorController.php
class RoleSelectorController extends Controller {
public function show(Request $r) {
$roles = $r->user()->roles()->pluck('name');
return view('auth.select-role', compact('roles'));
}
public function set(Request $r) {
$r->validate(['role'=>'required|string']);
abort_unless($r->user()->hasRole($r->role), 403);
session(['active_role' => $r->role]);
return redirect()->intended(route('admin.dashboard'));
}
}
Blocco 10 blade
{{-- resources/views/auth/select-role.blade.php --}}
@extends('layouts.app')
@section('content')
<h1>Seleziona ruolo</h1>
<form method="POST" action="{{ route('auth.role.set') }}">
@csrf
<select name="role" required>
@foreach($roles as $role)
<option value="{{ $role }}">{{ ucfirst($role) }}</option>
@endforeach
</select>
<button type="submit">Continua</button>
</form>
@endsection
Blocco 11 php
// app/Providers/AuthServiceProvider.php
Gate::before(function ($user, $ability) {
return $user->hasRole('super-admin') ? true : null;
});
Blocco 12 php
// app/Models/Menu.php
class Menu extends Model {
protected $fillable = ['name','description','is_active'];
public function items() { return $this->hasMany(MenuItem::class); }
}
// app/Models/MenuItem.php
class MenuItem extends Model {
protected $fillable = [
'menu_id','parent_id','label','route_name','url','icon','description',
'order_index','is_visible','required_roles','required_permissions'
];
protected $casts = ['required_roles'=>'array','required_permissions'=>'array'];
public function parent(){ return $this->belongsTo(MenuItem::class,'parent_id'); }
public function children(){ return $this->hasMany(MenuItem::class,'parent_id'); }
}
Blocco 13 php
// app/Services/MenuService.php
class MenuService {
public function forUserMenu(string $menuName, ?User $user): array {
$menu = Menu::where('name',$menuName)->first();
if (!$menu) return [];
$items = $menu->items()->orderBy('order_index')->get()->groupBy('parent_id');
$activeRole = session('active_role');
$filter = function($item) use ($user,$activeRole) {
if (!$item->is_visible) return false;
if ($item->required_roles && $activeRole && !in_array($activeRole, $item->required_roles)) return false;
if ($item->required_permissions) {
foreach ($item->required_permissions as $perm) {
if (!$user || !$user->can($perm)) return false;
}
}
return true;
};
$build = function($parentId) use (&$build, $items, $filter) {
return ($items[$parentId] ?? collect())->filter($filter)->map(function($i) use (&$build) {
return [
'id'=>$i->id,'label'=>$i->label,'route_name'=>$i->route_name,'url'=>$i->url,'icon'=>$i->icon,
'children'=>$build($i->id)->values()->all()
];
});
};
return $build(null)->values()->all();
}
}
Blocco 14 blade
{{-- resources/views/layouts/admin.blade.php --}}
@php($menu = app(\App\Services\MenuService::class)->forUserMenu('admin_main', auth()->user()))
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title','sartUP Admin')</title>
@vite(['resources/css/app.css','resources/js/app.js'])
</head>
<body class="min-h-screen bg-gray-50">
<header class="h-14 shadow flex items-center px-4 bg-white">
<nav class="flex gap-4">
@foreach($menu as $item)
<a href="{{ $item['route_name'] ? route($item['route_name']) : ($item['url'] ?? '#') }}" class="font-medium">
{{ $item['label'] }}
</a>
@endforeach
</nav>
<div class="ml-auto">
<form method="POST" action="{{ route('logout') }}">@csrf<button>Logout</button></form>
</div>
</header>
<div class="flex">
<aside class="w-64 bg-white border-r min-h-[calc(100vh-3.5rem)] p-3">
{{-- sidebar: figli della voce L1 corrente → per demo, mostra tutti i figli del primo item --}}
@if(count($menu))
@foreach(($menu[1]['children'] ?? $menu[0]['children'] ?? []) as $child)
<div class="mb-3">
<div class="font-semibold">{{ $child['label'] }}</div>
@if(count($child['children']))
<ul class="ml-3 list-disc">
@foreach($child['children'] as $sub)
<li><a href="{{ $sub['route_name'] ? route($sub['route_name']) : ($sub['url'] ?? '#') }}">{{ $sub['label'] }}</a></li>
@endforeach
</ul>
@endif
</div>
@endforeach
@endif
</aside>
<main class="flex-1 p-6">
@yield('content')
</main>
</div>
</body>
</html>
Blocco 15 php
// app/Http/Controllers/Admin/I40/MachinesController.php
namespace App\Http\Controllers\Admin\I40;
class MachinesController extends Controller {
public function connected() {
return view('admin.i40.machines.connected');
}
}
Blocco 16 blade
{{-- resources/views/admin/i40/machines/connected.blade.php --}}
@extends('layouts.admin')
@section('title','Elenco macchine collegate')
@section('content')
<h1 class="text-xl font-semibold mb-4">Elenco macchine collegate</h1>
<table class="min-w-full bg-white shadow border">
<thead><tr>
<th class="p-2 text-left">Macchina</th>
<th class="p-2 text-left">Protocollo</th>
<th class="p-2 text-left">Stato</th>
<th class="p-2 text-left">Last seen</th>
</tr></thead>
<tbody>
<tr><td class="p-2">—</td><td class="p-2">—</td><td class="p-2">—</td><td class="p-2">—</td></tr>
</tbody>
</table>
@endsection